import { RenovateConfig } from '../../../config';
import { REPOSITORY_NO_VULNERABILITY } from '../../../constants/error-messages';
import * as datasourceMaven from '../../../datasource/maven';
import * as datasourceNpm from '../../../datasource/npm';
import * as datasourceNuget from '../../../datasource/nuget';
import * as datasourcePypi from '../../../datasource/pypi';
import * as datasourceRubygems from '../../../datasource/rubygems';
import { logger } from '../../../logger';
import { platform } from '../../../platform';
import { SecurityAdvisory } from '../../../types';
import * as allVersioning from '../../../versioning';
import * as mavenVersioning from '../../../versioning/maven';
import * as npmVersioning from '../../../versioning/npm';
import * as pep440Versioning from '../../../versioning/pep440';
import * as rubyVersioning from '../../../versioning/ruby';
import * as semverVersioning from '../../../versioning/semver';

type CombinedAlert = Record<
  string,
  Record<
    string,
    {
      advisories: SecurityAdvisory[];
      fileNames: string[];
      firstPatchedVersion?: string;
      vulnerableRequirements?: string;
    }
  >
>;

export async function detectVulnerabilityAlerts(
  input: RenovateConfig
): Promise<RenovateConfig> {
  if (!(input && input.vulnerabilityAlerts)) {
    return input;
  }
  if (input.vulnerabilityAlerts.enabled === false) {
    logger.debug('Vulnerability alerts are disabled');
    return input;
  }
  const alerts = await platform.getVulnerabilityAlerts();
  if (!alerts.length) {
    logger.debug('No vulnerability alerts found');
    if (input.vulnerabilityAlertsOnly) {
      throw new Error(REPOSITORY_NO_VULNERABILITY);
    }
    return input;
  }
  const config = { ...input };
  const combinedAlerts: CombinedAlert = {};
  for (const alert of alerts) {
    try {
      if (alert.dismissReason) {
        continue; // eslint-disable-line no-continue
      }
      if (!alert.securityVulnerability.firstPatchedVersion) {
        logger.debug(
          { alert },
          'Vulnerability alert has no firstPatchedVersion - skipping'
        );
        continue; // eslint-disable-line no-continue
      }
      const datasourceMapping: Record<string, string> = {
        MAVEN: datasourceMaven.id,
        NPM: datasourceNpm.id,
        NUGET: datasourceNuget.id,
        PIP: datasourcePypi.id,
        RUBYGEMS: datasourceRubygems.id,
      };
      const datasource =
        datasourceMapping[alert.securityVulnerability.package.ecosystem];
      if (!combinedAlerts[datasource]) {
        combinedAlerts[datasource] = {};
      }
      const depName = alert.securityVulnerability.package.name;
      if (!combinedAlerts[datasource][depName]) {
        combinedAlerts[datasource][depName] = {
          advisories: [],
          fileNames: [],
        };
      }
      combinedAlerts[datasource][depName].advisories.push(
        alert.securityAdvisory
      );
      const fileName = alert.vulnerableManifestFilename;
      if (!combinedAlerts[datasource][depName].fileNames.includes(fileName)) {
        combinedAlerts[datasource][depName].fileNames.push(fileName);
      }
      const firstPatchedVersion =
        alert.securityVulnerability.firstPatchedVersion.identifier;
      const versionings: Record<string, string> = {
        maven: mavenVersioning.id,
        npm: npmVersioning.id,
        nuget: semverVersioning.id,
        pip_requirements: pep440Versioning.id,
        rubygems: rubyVersioning.id,
      };
      const version = allVersioning.get(versionings[datasource]);
      if (version.isVersion(firstPatchedVersion)) {
        if (combinedAlerts[datasource][depName].firstPatchedVersion) {
          if (
            version.isGreaterThan(
              firstPatchedVersion,
              combinedAlerts[datasource][depName].firstPatchedVersion
            )
          ) {
            combinedAlerts[datasource][
              depName
            ].firstPatchedVersion = firstPatchedVersion;
            combinedAlerts[datasource][depName].vulnerableRequirements =
              alert.vulnerableRequirements;
          }
        } else {
          combinedAlerts[datasource][
            depName
          ].firstPatchedVersion = firstPatchedVersion;
          combinedAlerts[datasource][depName].vulnerableRequirements =
            alert.vulnerableRequirements;
        }
      } else {
        logger.debug('Invalid firstPatchedVersion: ' + firstPatchedVersion);
      }
    } catch (err) {
      logger.warn({ err }, 'Error parsing vulnerability alert');
    }
  }
  const alertPackageRules = [];
  for (const [datasource, dependencies] of Object.entries(combinedAlerts)) {
    for (const [depName, val] of Object.entries(dependencies)) {
      let prBodyNotes: string[] = [];
      try {
        prBodyNotes = ['### GitHub Vulnerability Alerts'].concat(
          val.advisories.map((advisory) => {
            let content = '#### ';
            let heading;
            if (advisory.identifiers.some((id) => id.type === 'CVE')) {
              heading = advisory.identifiers
                .filter((id) => id.type === 'CVE')
                .map((id) => id.value)
                .join(' / ');
            } else {
              heading = advisory.identifiers.map((id) => id.value).join(' / ');
            }
            if (advisory.references.length) {
              heading = `[${heading}](${advisory.references[0].url})`;
            }
            content += heading;
            content += '\n\n';
            content += advisory.description;
            return content;
          })
        );
      } catch (err) /* istanbul ignore next */ {
        logger.warn({ err }, 'Error generating vulnerability PR notes');
      }
      let matchCurrentVersion = val.vulnerableRequirements;
      // istanbul ignore if
      if (!matchCurrentVersion) {
        if (datasource === datasourceMaven.id) {
          matchCurrentVersion = `(,${val.firstPatchedVersion})`;
        } else {
          matchCurrentVersion = `< ${val.firstPatchedVersion}`;
        }
      }
      const allowedVersions =
        datasource === datasourcePypi.id
          ? `==${val.firstPatchedVersion}`
          : val.firstPatchedVersion;
      const matchRule = {
        datasources: [datasource],
        packageNames: [depName],
        matchCurrentVersion,
        allowedVersions,
        prBodyNotes,
        force: {
          ...config.vulnerabilityAlerts,
          vulnerabilityAlert: true,
          branchTopic: `${datasource}-${depName}-vulnerability`,
        },
      };
      alertPackageRules.push(matchRule);
    }
  }
  logger.debug({ alertPackageRules }, 'alert package rules');
  config.packageRules = (config.packageRules || []).concat(alertPackageRules);
  return config;
}
